/*
 * Copyright 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.health.connect.client.records

import androidx.health.connect.client.records.metadata.Metadata
import java.time.Duration
import java.time.Instant
import java.time.LocalDate
import java.time.LocalTime
import java.time.ZoneId
import java.time.ZoneOffset

/**
 * Captures a planned exercise session, also commonly referred to as a training plan.
 *
 * <p>Each record contains a start time, end time, an exercise type and a list of
 * [PlannedExerciseBlock] which describe the details of the planned session. The start and end times
 * may be in the future.
 *
 * Requires [androidx.health.connect.client.HealthConnectFeatures.FEATURE_PLANNED_EXERCISE].
 */
class PlannedExerciseSessionRecord
internal constructor(
    override val startTime: Instant,
    override val startZoneOffset: ZoneOffset?,
    override val endTime: Instant,
    override val endZoneOffset: ZoneOffset?,
    override val metadata: Metadata,
    @get:JvmName("hasExplicitTime") val hasExplicitTime: Boolean,
    /** Type of exercise (e.g. walking, swimming). Required field. */
    @property:ExerciseSessionRecord.ExerciseTypes val exerciseType: Int,
    /** The exercise session that completed this planned session. */
    val completedExerciseSessionId: String?,
    val blocks: List<PlannedExerciseBlock>,
    /** Title of the session. Optional field. */
    val title: String? = null,
    /** Additional notes for the session. Optional field. */
    val notes: String? = null,
) : IntervalRecord {
    /**
     * Constructor that accepts a physical time and zone offset.
     *
     * @param startTime The time at which the session should start.
     * @param startZoneOffset The zone offset at the start of this session. If null is provided,
     *   this will default to the current system timezone.
     * @param endTime The time at which the session should end.
     * @param endZoneOffset The zone offset at the end of this session. If null is provided, this
     *   will default to the current system timezone.
     * @param blocks The [PlannedExerciseBlock]s that contain details of this session.
     * @param title The title of this session.
     * @param notes Notes for this session.
     * @param exerciseType The exercise type of this session.
     * @param metadata Metadata for this session.
     */
    @JvmOverloads
    constructor(
        startTime: Instant,
        startZoneOffset: ZoneOffset?,
        endTime: Instant,
        endZoneOffset: ZoneOffset?,
        metadata: Metadata,
        blocks: List<PlannedExerciseBlock>,
        /** Type of exercise (e.g. walking, swimming). Required field. */
        exerciseType: Int,
        /** Title of the session. Optional field. */
        title: String? = null,
        /** Additional notes for the session. Optional field. */
        notes: String? = null,
    ) : this(
        startTime,
        startZoneOffset,
        endTime,
        endZoneOffset,
        metadata,
        true,
        exerciseType,
        null,
        blocks,
        title = title,
        notes = notes,
    )

    /**
     * Constructor that accepts a local date plus a duration. The start time will be implicitly
     * generated from a local time at noon in the system default timezone on the day specified by
     * [startDate]. The end time will be generated by adding [duration] to the start time.
     *
     * @param startDate The date on which the session should occur.
     * @param duration The expected or estimated duration of the session.
     * @param blocks The [PlannedExerciseBlock]s that contain details of this session.
     * @param title The title of this session.
     * @param notes Notes for this session.
     * @param exerciseType The exercise type of this session.
     * @param metadata Metadata for this session.
     */
    @JvmOverloads
    constructor(
        metadata: Metadata,
        startDate: LocalDate,
        duration: Duration,
        blocks: List<PlannedExerciseBlock>,
        /** Type of exercise (e.g. walking, swimming). Required field. */
        exerciseType: Int,
        /** Title of the session. Optional field. */
        title: String? = null,
        /** Additional notes for the session. Optional field. */
        notes: String? = null,
    ) : this(
        startDate.toPhysicalTimeAtNoon(),
        startDate.toPhysicalTimeAtNoon().getOffset(),
        startDate.toPhysicalTimeAtNoon().plus(duration),
        startDate.toPhysicalTimeAtNoon().plus(duration).getOffset(),
        metadata,
        false,
        exerciseType,
        null,
        blocks,
        title = title,
        notes = notes,
    )

    init {
        require(startTime.isBefore(endTime))
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is PlannedExerciseSessionRecord) return false

        if (startTime != other.startTime) return false
        if (startZoneOffset != other.startZoneOffset) return false
        if (endTime != other.endTime) return false
        if (endZoneOffset != other.endZoneOffset) return false
        if (hasExplicitTime != other.hasExplicitTime) return false
        if (blocks != other.blocks) return false
        if (title != other.title) return false
        if (notes != other.notes) return false
        if (exerciseType != other.exerciseType) return false
        if (metadata != other.metadata) return false

        return true
    }

    override fun hashCode(): Int {
        var result = startTime.hashCode()
        result = 31 * result + (startZoneOffset?.hashCode() ?: 0)
        result = 31 * result + endTime.hashCode()
        result = 31 * result + (endZoneOffset?.hashCode() ?: 0)
        result = 31 * result + hasExplicitTime.hashCode()
        result = 31 * result + blocks.hashCode()
        result = 31 * result + (title?.hashCode() ?: 0)
        result = 31 * result + (notes?.hashCode() ?: 0)
        result = 31 * result + exerciseType
        result = 31 * result + (completedExerciseSessionId?.hashCode() ?: 0)
        result = 31 * result + metadata.hashCode()
        return result
    }

    override fun toString(): String {
        return "PlannedExerciseSessionRecord(startTime=$startTime, startZoneOffset=$startZoneOffset, endTime=$endTime, endZoneOffset=$endZoneOffset, hasExplicitTime=$hasExplicitTime, title=$title, notes=$notes, exerciseType=$exerciseType, completedExerciseSessionId=$completedExerciseSessionId, metadata=$metadata, blocks=$blocks)"
    }

    companion object {
        /**
         * Converts a local date to a physical timestamp by assuming a fixed time at noon and the
         * current system time zone.
         */
        private fun LocalDate.toPhysicalTimeAtNoon(): Instant {
            return this.atTime(LocalTime.NOON).atZone(ZoneId.systemDefault()).toInstant()
        }

        /** Gets the offset of the system default timezone at the given [Instant]. */
        private fun Instant.getOffset(): ZoneOffset {
            return ZoneOffset.systemDefault().rules.getOffset(this)
        }
    }
}
